﻿/*
Copyright (c) 2008 William Duff

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Net;
using LiveUpload.YouTube.Properties;
using System.Threading;
using System.Text.RegularExpressions;

namespace LiveUpload.YouTube
{
    /// <summary>
    /// This form brings the user through standard configuration of the video upload, including 
    /// choosing a YouTube account, adding a title, description, and keywords, choosing a category
    /// and choosing if the video should be public or private. The form validates all of the fields 
    /// prior to saving the information to the XML files provided by the plugin framework.
    /// </summary>
    internal partial class ConfigureForm : Form
    {
        /// <summary>
        /// Constructs an empty configuration form.
        /// </summary>
        internal ConfigureForm(SessionData sessionData, PersistentData persistData)
        {
            // Establish an exception handler for this class.
            Application.ThreadException += 
                new ThreadExceptionEventHandler(ConfigurationSettings_ThreadException);

            this.sessionData = sessionData;
            this.persistData = persistData;
            this.publishPanelInitialized = false;

            InitializeComponent();
            PreInitializePublishPanel();
        }

        // Private variables.
        private SessionData sessionData;
        private PersistentData persistData;
        private YouTubeApi youTube;
        private int numAuthorizationWorkers;
        private bool publishPanelInitialized;

        /// <summary>
        /// Enumerates all of the panels that exist in the configuration form.
        /// </summary>
        private enum ConfigurePanel
        {
            ApiKeys,
            Login,
            Publish,
            PrivacyPolicy
        };

        /// <summary>
        /// A short and sweet subclassed Label that enables the transparent background color.
        /// </summary>
        private class TransLabel : Label
        {
            public TransLabel()
                : base()
            {
                this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            }
        }

        /// <summary>
        /// A short and sweet subclassed LinkLabel that enables the transparent background color.
        /// </summary>
        private class TransLinkLabel : LinkLabel
        {
            public TransLinkLabel()
                : base()
            {
                this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            }
        }

        /// <summary>
        /// This event is fired during the call to ShowDialog.
        /// </summary>
        private void ConfigureForm_Load(object sender, EventArgs e)
        {
            if (this.persistData.DeveloperKey == string.Empty)
            {
                ShowPanel(ConfigurePanel.ApiKeys);
            }
            else
            {
                this.youTube =
                    new YouTubeApi(this.persistData.DeveloperKey, this.persistData.ClientId);
                
                if (this.persistData.Users.Count == 0)
                {
                    ShowPanel(ConfigurePanel.Login);
                }
                else
                {
                    ShowPanel(ConfigurePanel.Publish);
                }
            }
        }

        /// <summary>
        /// Handles any unhandled exceptions thrown by the ConfigureForm by popping a dialog with 
        /// the error message and then closing ConfigureForm.
        /// </summary>
        private void ConfigurationSettings_ThreadException(object sender, 
            ThreadExceptionEventArgs e)
        {
            ShowError(string.Format(Resources.UnexpectedError, e.Exception.Message), true);
        }

        /// <summary>
        /// Displays a simple message box to alert the user that a non-fatal error has occurred.
        /// </summary>
        /// <param name="errorMessage">
        /// The error message to display.
        /// </param>
        private void ShowError(string errorMessage)
        {
            ShowError(errorMessage, false);
        }

        /// <summary>
        /// Displays a simple message box to alert the user that an error has occurred. If the 
        /// error is fatal, the publish session is terminated.
        /// </summary>
        /// <param name="errorMessage">
        /// The error message to display.
        /// </param>
        /// <param name="fatal">
        /// true if the error is fatal and false otherwise.
        /// </param>
        private void ShowError(string errorMessage, bool isFatalError)
        {
            MessageBox.Show(errorMessage, Resources.PluginTitle, MessageBoxButtons.OK,
                MessageBoxIcon.Error);

            if (isFatalError)
            {
                this.DialogResult = DialogResult.Cancel;
                this.Close();
            }
        }

        /// <summary>
        /// Switches to the specified panel.
        /// </summary>
        /// <param name="panel">
        /// The panel to switch to.
        /// </param>
        private void ShowPanel(ConfigurePanel panel)
        {
            this.SuspendLayout();

            this.apiPanel.Hide();
            this.loginPanel.Hide();
            this.publishPanel.Hide();
            this.privacyPanel.Hide();

            switch (panel)
            {
                case ConfigurePanel.ApiKeys:
                    this.AcceptButton = this.apiNextButton;
                    this.CancelButton = this.apiCancelButton;
                    this.apiPanel.Show();
                    this.apiDeveloperKeyTextBox.Focus();
                    break;
                case ConfigurePanel.Login:
                    this.AcceptButton = this.loginSignInButton;
                    this.CancelButton = this.loginCancelButton;
                    this.loginPanel.Show();
                    this.loginUsernameTextBox.Focus();
                    break;
                case ConfigurePanel.Publish:
                    this.AcceptButton = this.publishButton;
                    this.CancelButton = this.publishCancelButton;
                    this.publishPanel.Show();
                    this.publishVideoTitleTextBox.Focus();
                    break;
                case ConfigurePanel.PrivacyPolicy:
                    this.AcceptButton = this.privacyBackButton;
                    this.CancelButton = null;
                    this.privacyPanel.Show();
                    this.privacyBackButton.Focus();
                    break;
            }

            this.ResumeLayout(true);
        }

        /// <summary>
        /// This event is fired when the user clicks the Cancel button on any page on the API panel.
        /// </summary>
        private void CancelButton_Click(object sender, EventArgs e)
        {
            // Cancel out of the dialog.
            this.DialogResult = DialogResult.Cancel;
            this.Close();
        }

        /// <summary>
        /// This event is fired when the user clicks on the 'Create YouTube API Key' link on the 
        /// API panel.
        /// </summary>
        private void apiServiceLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://code.google.com/apis/youtube/dashboard/");
        }

        /// <summary>
        /// This event is fired when the user changes the value of the developer key or the client 
        /// id on the API panel.
        /// </summary>
        private void apiDeveloperKeyOrClientId_TextChanged(object sender, EventArgs e)
        {
            this.apiNextButton.Enabled = this.apiClientIdTextBox.Text.Trim() != string.Empty &&
                this.apiDeveloperKeyTextBox.Text.Trim() != string.Empty;
            this.apiNextButton.Refresh();
        }

        /// <summary>
        /// This event is fired when the user clicks on the next button on the API panel.
        /// </summary>
        private void apiNextButton_Click(object sender, EventArgs e)
        {
            this.apiNextButton.Enabled = false;
            this.apiNextButton.Refresh();

            string developerKey = this.apiDeveloperKeyTextBox.Text.Trim();
            string clientId = this.apiClientIdTextBox.Text.Trim();

            this.youTube = new YouTubeApi(developerKey, clientId);
            this.persistData.DeveloperKey = developerKey;
            this.persistData.ClientId = clientId;

            ShowPanel(ConfigurePanel.Login);

            this.apiNextButton.Enabled = true;
            this.apiNextButton.Refresh();
        }

        /// <summary>
        /// This event is fired whenever the Login panel is shown or hidden.
        /// </summary>
        private void loginPanel_VisibleChanged(object sender, EventArgs e)
        {
            // Show or hide the back button to the Publish panel.
            this.loginBackButton.Visible = this.publishPanelInitialized && 
                this.persistData.Users.Count > 0;
            this.loginBackButton.Refresh();
        }

        /// <summary>
        /// This event is fired when the user clicks on the create YouTube account link on the 
        /// Login panel.
        /// </summary>
        private void loginCreateAccountLink_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://www.youtube.com/signup");
        }

        /// <summary>
        /// This event is fired when the user clicks on the forgot YouTube username link on the 
        /// Login panel.
        /// </summary>
        private void loginForgotUsernameLink_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://www.youtube.com/forgot_username");
        }

        /// <summary>
        /// This event is fired when the user clicks on the forgot YouTube password link on the 
        /// Login panel.
        /// </summary>
        private void loginForgotPasswordLink_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://www.youtube.com/forgot");
        }

        /// <summary>
        /// This event is fired when the user changes the value of the username or password on the
        /// Login panel.
        /// </summary>
        private void loginUsernameOrPassword_TextChanged(object sender, EventArgs e)
        {
            this.loginSignInButton.Enabled = this.loginUsernameTextBox.Text != string.Empty &&
                this.loginPasswordTextBox.Text != string.Empty;
            this.loginSignInButton.Refresh();
        }

        /// <summary>
        /// This event is fired when the user clicks on the sign in button on the Login panel.
        /// </summary>
        private void loginSignInButton_Click(object sender, EventArgs e)
        {
            this.loginSignInButton.Enabled = false;
            this.loginSignInButton.Refresh();

            string username = this.loginUsernameTextBox.Text;
            string password = this.loginPasswordTextBox.Text;

            try
            {
                string token = this.youTube.GetUserToken(username, password);
                if (token == null)
                {
                    ShowError(Resources.UnknownError);
                }
                else
                {
                    Account user = new Account(username, token);

                    this.persistData.DefaultUser = user;
                    AddUserToAccountComboBox(user);

                    ShowPanel(ConfigurePanel.Publish);
                }
            }
            catch (YouTubeApiException ye)
            {
                ShowError(ye.Message);
                
                // They probably mistyped their password, so select it for them.
                this.loginPasswordTextBox.Focus();
                this.loginPasswordTextBox.SelectAll();
            }
            finally
            {
                this.loginSignInButton.Enabled = true;
                this.loginSignInButton.Refresh();
            }
        }

        /// <summary>
        /// This event is fired when the user clicks on the back button on the Login panel.
        /// </summary>
        private void loginBackButton_Click(object sender, EventArgs e)
        {
            // Go ahead and show the Publish panel since it must have been initialized previously.
            ShowPanel(ConfigurePanel.Publish);

            // Default to the top of the list.
            this.publishAccountComboBox.SelectedIndex = 0;

            this.loginBackButton.Visible = false;
            this.loginBackButton.Refresh();
        }

        /// <summary>
        /// This event is fired when the visibility of the Publish panel changes.
        /// </summary>
        private void publishPanel_VisibleChanged(object sender, EventArgs e)
        {
            if (this.publishPanel.Visible)
            {
                InitializePublishPanel();
            }
        }

        /// <summary>
        /// Set up the Publish panel the way we want a user to see it while it's being intialized.
        /// </summary>
        private void PreInitializePublishPanel()
        {
            // Show loading status in account combobox.
            this.publishAccountComboBox.Items.Add(Resources.LoadingString);
            this.publishAccountComboBox.SelectedIndex = 0;

            // Show loading status in category combobox.
            this.publishVideoCategoryComboBox.Items.Add(Resources.LoadingString);
            this.publishVideoCategoryComboBox.SelectedIndex = 0;

            // Default permission combobox to public.
            this.publishVideoPermissionComboBox.Items.Insert(0, Resources.PublicPermissionString);
            this.publishVideoPermissionComboBox.Items.Insert(1, Resources.PrivatePermissionString);
            this.publishVideoPermissionComboBox.SelectedIndex = 0;

            // Initialize the panel with some information from the session data.
            this.publishVideoTitleTextBox.Text = 
                this.sessionData.ItemSet[0].OriginalFileName.Remove(
                this.sessionData.ItemSet[0].OriginalFileName.LastIndexOf('.'));

            if (this.sessionData.ItemSet[0].Title != string.Empty)
            {
                this.publishVideoDescriptionTextBox.ForeColor = Color.Black;
                this.publishVideoDescriptionTextBox.Font = new Font("Tahoma", 8.25F, FontStyle.Regular);
                this.publishVideoDescriptionTextBox.Text = this.sessionData.ItemSet[0].Title;
            }
        }

        /// <summary>
        /// Initializes the Publish panel by populating all necessary fields.
        /// </summary>
        private void InitializePublishPanel()
        {
            // Don't allow reentrancy.
            if (this.publishPanelInitialized)
            {
                return;
            }
            this.publishPanelInitialized = true;

            // Grab the current list of users.
            Account[] users = this.persistData.Users.ToArray();
            this.persistData.Users.Clear();

            // Keep track of how many background workers we're starting.
            this.numAuthorizationWorkers = users.Length;

            // Start a background worker for each user to see if their authentication token is 
            // stil valid. Valid accounts will be added back to the persistent user list.
            foreach (Account user in users)
            {
                BackgroundWorker authorizationWorker = new BackgroundWorker();
                authorizationWorker.WorkerSupportsCancellation = true;
                authorizationWorker.DoWork += new DoWorkEventHandler(authorizationWorker_DoWork);
                authorizationWorker.RunWorkerCompleted += 
                    new RunWorkerCompletedEventHandler(authorizationWorker_RunWorkerCompleted);
                authorizationWorker.RunWorkerAsync(user);
            }

            // Start one background worker to get a list of video categories from YouTube.
            BackgroundWorker categoriesWorker = new BackgroundWorker();
            categoriesWorker.DoWork += new DoWorkEventHandler(categoriesWorker_DoWork);
            categoriesWorker.RunWorkerCompleted += 
                new RunWorkerCompletedEventHandler(categoriesWorker_RunWorkerCompleted);
            categoriesWorker.RunWorkerAsync();
        }

        /// <summary>
        /// Set up the Publish panel the way we want a user to see it after it's been initialized.
        /// </summary>
        private void PostInitializePublishPanel()
        {
            // It's possible no accounts were authorized because all of their tokens expired.
            if (this.persistData.Users.Count == 0)
            {
                ShowPanel(ConfigurePanel.Login);
                ShowError(Resources.AuthenticationTokenExpiredError);
                this.publishPanelInitialized = false;
            }
            else
            {
                // We're ready to show the list of accounts.
                this.publishAccountComboBox.Enabled = true;
                PopulateAccountComboBox();

                // Enable the remove link now that an account is available to be removed.
                this.publishRemoveAccountLink.Enabled = true;
                this.publishRemoveAccountLink.Refresh();
            }
        }

        /// <summary>
        /// Checks if the specified user's authentication token is still valid. All the work is 
        /// done on a background worker thread so the UI thread is not tied up.
        /// </summary>
        private void authorizationWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            Account user = (Account)e.Argument;

            // Pass the user as the result.
            e.Result = user;

            // Cancel if the token is not valid.
            e.Cancel = !YouTubeApi.UserTokenIsValid(user);
        }

        /// <summary>
        /// Once the background worker returns, the UI thread runs this function. If there was no
        /// error, the user must have been authorized successfully.
        /// </summary>
        private void authorizationWorker_RunWorkerCompleted(object sender, 
            RunWorkerCompletedEventArgs e)
        {
            // This is thread safe because this function is run on the UI thread instead of the
            // background worker thread.
            this.numAuthorizationWorkers--;

            // Make sure there was no exception, the background wasn't canceled and that the
            // Publish panel is both visible and enabled.
            if (e.Error == null && !e.Cancelled && this.CanSelect)
            {
                AddUserToAccountComboBox((Account)e.Result);
            }

            if (this.numAuthorizationWorkers == 0)
            {
                PostInitializePublishPanel();
            }
        }

        /// <summary>
        /// Adds a user to the persistData.Users list and then repopulates the account combobox.
        /// </summary>
        /// <param name="user">
        /// The authorized user to add.
        /// </param>
        internal void AddUserToAccountComboBox(Account user)
        {
            if (this.persistData.Users.Contains(user))
            {
                // Update the user.
                int userIndex = this.persistData.Users.IndexOf(user);
                this.persistData.Users[userIndex] = user;
            }
            else
            {
                this.persistData.Users.Add(user);
            }

            // Repopulate the account combobox if it has been enabled.
            if (this.publishAccountComboBox.Enabled)
            {
                PopulateAccountComboBox();
            }
        }

        /// <summary>
        /// Removes a user from the persistData.Users list and then repopulates the account 
        /// combobox.
        /// </summary>
        /// <param name="user">
        /// The user to remove.
        /// </param>
        internal void RemoveUserFromAccountComboBox(Account user)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            // Remove the user from the list of saved accounts.
            this.persistData.Users.Remove(user);

            // If the user is the default user, make sure we stay in a consistent state.
            if (user == this.persistData.DefaultUser)
            {
                if (this.persistData.Users.Count > 0)
                {
                    // Default to the first user in the list.
                    this.persistData.DefaultUser = this.persistData.Users[0];
                }
                else
                {
                    this.persistData.DefaultUser = null;
                }
            }

            // Repopulate the account combobox if it has been enabled.
            if (this.publishAccountComboBox.Enabled)
            {
                PopulateAccountComboBox();
            }
        }

        /// <summary>
        /// Fills the account combobox with users from the user list in the persistent data.
        /// </summary>
        internal void PopulateAccountComboBox()
        {
            // Users may be added to the account combobox asynchronously, so sorting ensures a 
            // consistent ordering.
            this.persistData.Users.Sort(delegate(Account user1, Account user2)
            {
                return Comparer<string>.Default.Compare(user1.Name, user2.Name);
            });

            this.publishAccountComboBox.SuspendLayout();
            this.publishAccountComboBox.Items.Clear();
            foreach (Account user in this.persistData.Users)
            {
                this.publishAccountComboBox.Items.Add(user);
            }
            this.publishAccountComboBox.Items.Add(Resources.AddAccountString);
            if (this.persistData.DefaultUser == null)
            {
                this.publishAccountComboBox.SelectedIndex = 0;
            }
            else
            {
                this.publishAccountComboBox.SelectedItem = this.persistData.DefaultUser;
            }
            this.publishAccountComboBox.ResumeLayout(true);
        }

        /// <summary>
        /// This event is fired every time the selected index is changed in the account combobox on 
        /// the Publish panel.
        /// </summary>
        private void publishAccountComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            string selected = this.publishAccountComboBox.SelectedItem.ToString();
            if (selected == Resources.AddAccountString && this.publishPanel.Visible)
            {
                ShowPanel(ConfigurePanel.Login);
            }
        }

        /// <summary>
        /// This event is fired when the user clicks on the Remove link on the Publish panel.
        /// </summary>
        private void publishRemoveAccountLink_LinkClicked(object sender,
            LinkLabelLinkClickedEventArgs e)
        {
            Account user = this.publishAccountComboBox.SelectedItem as Account;
            if (user != null)
            {
                RemoveUserFromAccountComboBox(user);
            }
        }

        /// <summary>
        /// Downloads a list of video categories from YouTube. All the work is done on a background 
        /// worker thread so the UI thread is not tied up.
        /// </summary>
        private void categoriesWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            e.Result = YouTubeApi.GetCategories();
        }

        /// <summary>
        /// Once the background worker returns, the UI thread runs this function. If there was an
        /// error, alert the user. Otherwise, populate the category combobox.
        /// </summary>
        private void categoriesWorker_RunWorkerCompleted(object sender,
            RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                ShowError(Resources.FailedToLoadCategoriesError, true);
            }
            else if (!e.Cancelled && this.CanSelect)
            {
                PopulateCategoriesComboBox((List<Category>)e.Result);
            }
        }

        /// <summary>
        /// Once the request to get the video categories from YouTube has completed, this function 
        /// is called to populate the category combobox on the Publish panel.
        /// </summary>
        /// <param name="categories">
        /// A list of categories.
        /// </param>
        internal void PopulateCategoriesComboBox(List<Category> categories)
        {
            if (categories == null)
            {
                throw new ArgumentNullException("categories");
            }

            this.publishVideoCategoryComboBox.SuspendLayout();
            this.publishVideoCategoryComboBox.Enabled = true;
            this.publishVideoCategoryComboBox.Items.Clear();
            this.publishVideoCategoryComboBox.Items.Add(Resources.SelectCategoryString);
            categories.Sort(delegate(Category c1, Category c2)
            {
                return Comparer<string>.Default.Compare(c1.Label, c2.Label);
            });
            foreach (Category category in categories)
            {
                this.publishVideoCategoryComboBox.Items.Add(category);
            }
            this.publishVideoCategoryComboBox.SelectedIndex = 0;
            this.publishVideoCategoryComboBox.ResumeLayout(true);
        }

        /// <summary>
        /// This event is fired when the user enters a character into the title textbox.
        /// </summary>
        private void publishVideoTitleTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '<' || e.KeyChar == '>')
            {
                e.Handled = true;
                this.charNotAllowedToolTip.Show(Resources.TextCharacterNotAllowedString,
                    this.publishVideoTitleTextBox,
                    this.publishVideoTitleTextBox.Width / 2,
                    this.publishVideoTitleTextBox.Height, 3000);
            }
        }

        /// <summary>
        /// This event is fired when the user enters a character into the description textbox.
        /// </summary>
        private void publishVideoDescriptionTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '<' || e.KeyChar == '>')
            {
                e.Handled = true;
                this.charNotAllowedToolTip.Show(Resources.TextCharacterNotAllowedString,
                    this.publishVideoDescriptionTextBox,
                    this.publishVideoDescriptionTextBox.Width / 2,
                    this.publishVideoDescriptionTextBox.Height, 3000);

            }
        }

        /// <summary>
        /// This event is fired when the user enters a character into the keywords textbox.
        /// </summary>
        private void publishVideoKeywordsTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '<' || e.KeyChar == '>' || e.KeyChar == ',')
            {
                e.Handled = true;
                this.charNotAllowedToolTip.Show(Resources.KeywordCharacterNotAllowedString,
                    this.publishVideoKeywordsTextBox,
                    this.publishVideoKeywordsTextBox.Width / 2,
                    this.publishVideoKeywordsTextBox.Height, 3000);
            }
            else
            {
                this.keywordsErrorProvider.SetError(this.publishVideoKeywordsTextBox, "");
            }
        }

        /// <summary>
        /// This event is fired every time the user changes the value of any of the inputs. We 
        /// check that all of the required controls have valid input and then set the publish 
        /// button accordingly. Works from the bottom up in to take advantage of short circuiting.
        /// </summary>
        private void TryEnablePublishButton(object sender, EventArgs e)
        {
            this.publishButton.Enabled = 
                CategoryComboBoxSelectionIsValid() && !KeywordsTextBoxIsEmpty() && 
                DescriptionTextBoxIsValid() && TitleTextBoxIsValid() && 
                AccountComboBoxSelectionIsValid();
            this.publishButton.Refresh();
        }

        /// <summary>
        /// Checks to see if an account is selected in the account combobox.
        /// </summary>
        /// <returns>
        /// true if an account is selected and false otherwise.
        /// </returns>
        private bool AccountComboBoxSelectionIsValid()
        {
            Account user = this.publishAccountComboBox.SelectedItem as Account;
            return user != null;
        }

        /// <summary>
        /// Checks if the text in the title textbox is valid.
        /// </summary>
        /// <returns>
        /// true if the title is valid and false otherwise.
        /// </returns>
        private bool TitleTextBoxIsValid()
        {
            return this.publishVideoTitleTextBox.ForeColor != Color.Gray &&
                YouTubeApi.SanitizeString(this.publishVideoTitleTextBox.Text) != string.Empty;
        }

        /// <summary>
        /// Checks if the text in the description textbox is valid.
        /// </summary>
        /// <returns>
        /// true if the title is valid and false otherwise.
        /// </returns>
        private bool DescriptionTextBoxIsValid()
        {
            return this.publishVideoDescriptionTextBox.ForeColor != Color.Gray && 
                YouTubeApi.SanitizeString(this.publishVideoDescriptionTextBox.Text) != string.Empty;
        }

        /// <summary>
        /// Checks if the text in the keywords textbox is empty or equal to the "Required" string.
        /// </summary>
        /// <returns>
        /// true if the keywords textbox is empty or the text is gray.
        /// </returns>
        private bool KeywordsTextBoxIsEmpty()
        {
            return this.publishVideoKeywordsTextBox.ForeColor == Color.Gray || 
                YouTubeApi.SanitizeKeywords(this.publishVideoKeywordsTextBox.Text) == string.Empty;
        }

        /// <summary>
        /// Checks if the text in the keywords textbox is valid.
        /// </summary>
        /// <returns>
        /// true if the keywords are valid and false otherwise.
        /// </returns>
        private bool KeywordsTextBoxIsValid()
        {
            return !KeywordsTextBoxIsEmpty() && 
                YouTubeApi.KeywordsAreValid(this.publishVideoKeywordsTextBox.Text);
        }

        /// <summary>
        /// Checks to see if a category is selected in the category combobox.
        /// </summary>
        /// <returns>
        /// true if a category is selected and false otherwise.
        /// </returns>
        private bool CategoryComboBoxSelectionIsValid()
        {
            return this.publishVideoCategoryComboBox.SelectedItem.ToString() != 
                Resources.SelectCategoryString;
        }

        /// <summary>
        /// This event is fired when the user clicks out of the title, description or keyword 
        /// textboxes on the Publish panel. If the textbox is empty, it writes "Required" in gray, 
        /// italic text.
        /// </summary>
        private void WriteRequired_Leave(object sender, EventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            if (textBox.Text.Trim() == string.Empty)
            {
                textBox.ForeColor = Color.Gray;
                textBox.Font = new Font("Tahoma", 8.25F, FontStyle.Italic);
                textBox.Text = Resources.RequiredString;
                textBox.Refresh();
            }
        }

        /// <summary>
        /// This event is fired when the user clicks into the title, description of keyword 
        /// textboxes on the Publish panel. If the textbox has gray text (meaning it has "Required" 
        /// written in it), this function resets the forecolor to black and empties the text.
        /// </summary>
        private void RemoveRequired_Enter(object sender, EventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            if (textBox.ForeColor == Color.Gray)
            {
                textBox.ForeColor = Color.Black;
                textBox.Font = new Font("Tahoma", 8.25F, FontStyle.Regular);
                textBox.Text = string.Empty;
                textBox.Refresh();
            }
        }
        
        /// <summary>
        /// This event is fired when the user clicks on the Publish button on the Publish panel.
        /// </summary>
        private void publishButton_Click(object sender, EventArgs e)
        {
            // Let the user know if any of the keywords are invalid.
            if (!KeywordsTextBoxIsValid())
            {
                int startIndex;
                int length;
                YouTubeApi.FindKeywordError(this.publishVideoKeywordsTextBox.Text, out startIndex,
                    out length);

                this.publishVideoKeywordsTextBox.Focus();
                this.publishVideoKeywordsTextBox.Select(startIndex, length);

                this.keywordsErrorProvider.SetError(this.publishVideoKeywordsTextBox,
                    Resources.InvalidKeywordsError);

                return;
            }

            Account selectedUser = this.publishAccountComboBox.SelectedItem as Account;
            Category selectedCategory = this.publishVideoCategoryComboBox.SelectedItem as Category;

            // Save modifications to persistent data.
            this.persistData.DefaultUser = selectedUser;
            this.persistData.Save();

            // Save necessary info to sessionData.
            this.sessionData.DeveloperKey = this.persistData.DeveloperKey;
            this.sessionData.ClientId = this.persistData.ClientId;
            this.sessionData.User = selectedUser;
            this.sessionData.ItemSet[0].Title = 
                YouTubeApi.SanitizeString(this.publishVideoTitleTextBox.Text);
            this.sessionData.ItemSet[0].Description = 
                YouTubeApi.SanitizeString(this.publishVideoDescriptionTextBox.Text);
            
            string[] keywords = 
                YouTubeApi.SanitizeKeywords(this.publishVideoKeywordsTextBox.Text).Split(
                new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
            this.sessionData.ItemSet[0].KeywordSet = new List<string>(keywords);

            this.sessionData.ItemSet[0].Category = selectedCategory.Term;
            this.sessionData.IsPublic = 
                this.publishVideoPermissionComboBox.SelectedItem.ToString() == 
                Resources.PublicPermissionString;

            this.sessionData.Save();

            // We're done with the ConfigureForm class and ready for the next step in the upload
            // process.
            this.DialogResult = DialogResult.OK;
            this.Close();
        }

        /// <summary>
        /// This event is fired when the user clicks on the Privacy Policy link on the Publish 
        /// panel.
        /// </summary>
        private void publishPrivacyLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            ShowPanel(ConfigurePanel.PrivacyPolicy);
        }

        /// <summary>
        /// This event is fired when the user clicks on the Back button on the Privacy Policy panel.
        /// </summary>
        private void privacyBackButton_Click(object sender, EventArgs e)
        {
            ShowPanel(ConfigurePanel.Publish);
        }

        /// <summary>
        /// This event is fired when the user clicks on the YouTube Terms of Use link on the 
        /// Privacy Policy panel.
        /// </summary>
        private void privacyYouTubeTerms_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://youtube.com/t/terms");
        }

        /// <summary>
        /// This event is fired when the user clicks on the YouTube Privacy Policy link on the 
        /// Privacy Policy panel.
        /// </summary>
        private void privacyYouTubePrivacy_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://www.youtube.com/t/privacy");
        }

        /// <summary>
        /// This event is fired when the user clicks on the YouTube Guidelines link on the Privacy 
        /// Policy panel.
        /// </summary>
        private void privacyYouTubeGuidelines_LinkClicked(object sender, 
            LinkLabelLinkClickedEventArgs e)
        {
            Process.Start("http://www.youtube.com/t/community_guidelines");
        }
    }
}
